Installation

按照官网教程安装,可全局和单次,这里是用全局安装

terminal 输入 laravel 查看命令

  • laravel new projectName

  • 可以用server 软件,如MAPP 启动,也可以用自带的 php artisan serve 启动

  • 启动时,默认的目录是 project/public 文件夹

Basic Knowledge

Route & View

  • Route

    Route 在 app -> Http -> Request -> route.php 下,具体形式如

    Route::get('/', function () {		
        return view('welcome');			
    });
    
    1. 收到 “/” 请求,执行回调函数
    2. 返回一个叫做 “welcome” 的view
    • 也可以直接返回html, 如return "<h1>Hello World</h1>"
  • View

    View 在 resources -> views 下,具体形式为 welcome.blade.php

    • 如果view的位置在更深的目录,如 resources -> views -> pages -> about.blade.php ,那么在router里可以写成: return view('pages.welcome')

    使用了blade 模版引擎的一个view,在route 中可以用全称,也可以省略后缀

    //TODO view 可以理解成一个页面 还是 一个组件呢?

Data & Blade

  • Data

    data可以在定义router的时候被赋给不同的view, 如下

    Route::get('/datablade', function(){
        $people = ['Tom', "Ethan", "Eddy", "Cory"];
        return view('page.datablade', compact('people'));
    });
    

    这样,在datablade 这个view里面,我们就可以使用people里的数据了

    导入数据的方式还有很多种, 如 ['people'=>$people] , ->with('people'->$people) 等等

  • Blade

    Blade 是laravel 所使用的模版引擎,可以在blade.php 中写html 和 php,php需要 key wrod 和 closure key work 包裹起来使用,如

    <div class="container">
        @foreach($people as $person)
            <li>{{ $person }}</li>
        @endforeach
    </div>
    

所有@keywords 必须有 @endkeywords 来结束,中间可以把变量包裹在 {{}} 符号里

常用的keyword 如 if, for, foreach, unless 等等

Route & Controller

Controller 是将所有router 内函数定义信息分割出来的东西,看例子比较清晰

Laravel 会自动将服务器请求的数据转换为JSON格式,特别适合做API

  1. 首先,在 router.php 中,把原先的router 改成如下:

    //	Route::get('/datablade', function(){
    //	    $people = ['Tom', "Ethan", "Eddy", "Cory"];
    //	    return view('page.datablade', compact('people'));
    //	});
    
    Router::get('/', 'PagesController@datablade');
    

    其中,‘PagesController’ 是一个Controller,@datablade 是指其中对应该页面的方法

  2. 创建Controller

    controller 的路径: app -> Http -> Controller -> Controller.php

    自定义的话可以用 php artisan 来创建一个 controller 类,如

    $php artisan make:controller PagesController

  3. 定义PagesController

    class PagesController extends Controller
    {
        public function datablade(){
            $people = ['This', "Is", "Using", "Controller"];
            return view('pages.datablade', compact('people'));
        }
    }
    

    这里datablade 里的内容其实和原先router 里function内容一样,单独拎出来,叫做*“定义信息分割出来”*

    这样做的意义在于,如果要做的事情很多,比如处理request请求,就不适合放在router里面

  4. Url 指向不同内容时

    很多时候我们需要 http://example.com/card/1 这样的URL,这时在定义router 时我们可以如下获取参数

    Router::get('card/{cardId}', 'CardsController@show');
    

    这样card 后面的内容就会以 cardId 的名字存下来,可以在controller 里面进行操作,如下:

    public function show($id){
      $card = Card::find($id);
      return view(cards.show, compact('card'));
    }
    

    或者,有一种更好的方法,利用Eloquent 建的model 来做:

    public function show(Card $cardId){
      return view(cards.show, compact('cardId'));
    }
    

    这种方法直接找到 cardId 所对应的object,命名为$cardId,但是需注意,这里的命名需要和touter 里面的命名一致才可以,还需要有前面建好的Card 类

View

Layout Files (Master Page)

炒鸡实用的工具,想象每个页面都有一堆<mate>, <link>, <script>, 然后当我们需要修改其中任何一个,我们就必须修改每一个页面,非常麻烦,Layout file 很好的解决了这个痛点

  1. 在view 中新建一个 layout.blade.php ,并放入head 文件以及任何每个页面共有的内容,如下

    <!DOCTYPE html>
    <html>
    <head>
      	<mate view="portview" width="width-width" initial-scale=1 user-scalable=0>
        <title>Laravel</title>
        <link href="https://fonts.googleapis.com/css?family=Lato:100" rel="stylesheet" type="text/css">
        <link href="aa/bb/cc/abc.css" rel="stylesheet">
        <script type="text/javascript" src="...../..../..../.js"></script>
          
        @yield('headerSection')  
          
    </head>
    <body>
    
        @yield('bodySection')
      
      	@yield('footerSection')
      
    <script type="text/javascript" src="...../..../..../.js"></script>
    <script type="text/javascript" src="...../..../..../.js"></script>
    <script type="text/javascript" src="...../..../..../.js"></script>
    </body>
    </html>
    

    这个页面包含了所有页面公有的一些信息,把每个页面不同的body 留出来,叫做 “bodySection” (可自定义)

  2. 在view 的每个页面中,修改成如下信息

    @extends('layout')
    
    @section('bodySection')
    
        <div class="container">
            <div class="content">
                <div class="title">Laravel 5</div>
            </div>
        </div>
    
    @stop
    

    其中,@extends('layout') 会将layout file 里面包含的头文件等信息全部放进来,而我们的主要内容就放在 @section('bodySection')@stop 之间,这样就可以专注于内容

  3. 任何关于头文件的修改,只要在layout file 里面改一次即可全面应用

  4. headerSection 以及 footerScetion 可以用于特定页面需要加载特定内容的情况,与body用法一样

Database

database config coule be found at config -> database.php

1. 选择要使用的数据库

'default' => env('DB_CONNECTION', 'mysql'),

在这里选择默认的数据库类型,具体类型可参见下面connection里的内容,如sqllite,pgsql 等等

'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix' => '',
            'strict' => false,
            'engine' => null,
        ],
  ...

2. config 数据库

上面的代码里,注意env() 方法是读取到 /.env 文件中的配置

env('DB_HOST', 'localhost'), 的意思是在.env 配置文件中找不到DB_HOST项的话,默认使用第二个参数

.env 文件如下所示

...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=laravelTest
DB_USERNAME=root
DB_PASSWORD=********
...

基于这个机制,我们可以在推送代码到git 或者给别人时,直接ingore 这个.env 文件,只有在部署服务器的时候新建一个.env 文件,最大程度的保障了数据的安全

3. 使用Migration

Migration是一个数据库的版本管理工具,你可以rollback,reset等,它给予一种代码实现和命令行结合的方式来管理你的数据库。

alt

Migration 的位置在 database -> Magration ,默认有user 和 password,可以删掉重新建

  1. 建立magration

    $php artisan make:migration create_Cards_table --create='cards'

    可以在新建的magration 中添加字段

    Laravel 约定,数据表用首字母小写复数,model用首字母大写单数

  2. 更新migrate

    如果在已经建好表的情况下想要更新表的结构,需要新建一个update migrate

    $php artisan make:migration update_Cards_table

    在里面定义 更新 up 和 rollback down function 去实现更新操作,如

        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::table('blogs', function(Blueprint $table) {
                $table->string('createDate')->nullable()->change();
            });
        }
    
        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
            Schema::table('blogs', function(Blueprint $table) {
                $table->string('createDate')->nullable(false)->change();
            });
        }
    

    然后执行 $php artisan migrate

  3. 操作

    建表: $php artisan migrate

    回滚: $php artisan migrate:rollback

  4. Tinker

    artisan 的一种命令行工具,通过 $php artisan tinker 打开

    可以执行运算、php命令等等,下面的query-builder 就可以在tinker中先测试和修改

    可以使用 Control + C 退出tinker

4. Query Builder

insert:

DB::table('Card')->insert(['title' => 'new card', 'created_at' => new DateTime, 'Updated_at' => new DateTime]);

select:

DB::table("Card_Table")->get()

DB::table("Card_Table")->where("title", "new card")->first()

delete:

DB::table("Card_Table")->where("title", "new card")->delete()

5. Fetch Data

Query Builder 可以被写在 Controller 里面,如

class CardsController extends Controller
{
    public function index(){

        $cards = DB::table('Card_Table')->get();

        return view('cards.index', compact('cards'));
    }
}

如果报错找不到DB,则使用 use DB 在命名空间下面

6. Eloquent

Eloquent 是laravel 系统的ORM (Object Relation Mapping 关系映射),是laravel 最强大的地方

可以通过Eloquent 建 model的方式快速读取数据库,我们的 Model 类将继承自 Eloquent 提供的 Model 类,预置了很多异常强大的函数,从此想干啥事儿都是一行代码就搞定。

    1. 为某一张表新建model类

      $php artisan make:model Card

      这里 Card 是model 名字,Laravel 约定俗成数据表的名字小写复数,model的名字大写单数,这里对应的是表Cards

      新建好的model 在 app -> Card.php

      如果数据表和model 的名字不符合规定,则需要在model 里面加上 protected $table = 'tableName'; 来手动指定表格,建议每个model 都加上,保证准确性

      还可以通过 php artisan make:model name -m 的方式快速同时生成model 和 migration

    2. 在对应controller 里配置Eloquent 并访问数据

      use App\Card;
      ...
         public function index(){
              // 原先是: $cards = DB::table('cards')->get();
              $cards = Card::all();
              return view('cards.index', compact('cards'));
          }
      
    3. 一些简单操作的例子

      $card = Card::find(2);		//找到id 为2 的card
      $card = Card::where('title', '标题')->first();	//找到title 为‘标题’ 的card
      $cards = Card::all();		//返回全部card (此处是一个对象集合,可以加上->toArray() 转换为数组)
      $cards = Card::where('id', '>', 10)->where('id', '<', 20)->orderBy('updated_at', 'desc')->get();		//找到所有 id 大于10,小于20 的card,并且按照update_at 倒叙排序
      

      除了all 和 find方法外,其它所有的方法如where,orderBy 结尾的内容都要加上get() 或者 find() 来触发数据库查找的动作

7. Eloquent Relationship

在数据库操作中,把表格用foreign-key 连接在一起是很常见的操作

这里我们有一个 一对多 对应关系做例子:

​ card: id, ….

​ note: id, …, card_id

在Card 的model 里,可以写一个关系对应函数:

class Card extends Model
{
    public function notes()			//必须叫做notes 指向改表 不能修改
    {
        return $this->hasMany(Note::Class);
      				//hasOne/Many(Note, forigen_key, local_key)
    }
}

其中,Note::Class 还可以写成等价的 'App\Note'

这样,在Tinker里就可以测试 App\Card::find(3) -> notes; 会得到一个对应notes 的collection

  • 这里有一个小tricks,就是notes 是否需要带括号

    $card->notes 会返回一个collection,是一个sql语句查询的结果

    $card->notes() 会返回一个方法:Illuminate\Database\Eloquent\Relations\HasMany

    $card->notes->first() 会返回第一个note,但是是在查询得到所有notes后把第一个取出来

    $card->notes()->first() 会返回第一个note,但是是在SQL加入了条件,只从数据库里取到了一个数据

    两个方法效率截然不同,用的场合也不一样

反之,Note Model 也需要一个方法去联系到相应的 Card

class Note extends Model
{
    public function card()		//cards 指向改表 不能修改
    {
        return $this->belongsTo(Card::class);
    }
}

所有Eloquent 的返回结果均为collection object,有超级多的方法,比数据库好!

1 to 1

case: Phone table has a column called ‘user_id’, refers to the id column in User table

class User extends Model {
  public function phone() {
    return $this->hasOne('App\Phone');
  }
}

$phone = User::find(1)->phone;

如果 “user_id” 叫做 “other_id”,则使用 return $this->hasOne('App\Phone', 'other_id')

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

反之,我们还以通过phone 找到相应的user,叫做belongTo

class Phone extends Model {
  public function user() {
    return $this->belongTo('App\User')
  }
}
1 to many

一个user,可能对应多个phone

return $this->hasMany('App\Phone');

这个时候,所有user_id 为 1 的Phone 记录都会被返回

$phones = App\User::find(1)->phones;
foreach ($phones as $phone) {...}

反过来多对一,也是belongsTo 方法

Many to many

Case: User has many role_id, and specific role can refers to many people, so there’re 3 Tables:Users, Roles and user_role, 其中user 有id,roles 也有id,user-role 表把他们联系在一起

class User extends Model {
  public function roles() {
    return $this->belongsToMany('App\Role');
  }
}

$user = App\User::find(1);
$roles = $user->roles->orderBy('name')->get(); //or
foreach ($user->roles as $role){...}

Eloquent 会自动找 user_role table (named in alphabetical order),但是也可以自己定义表名和字段名

return $this->belongsToMany('App\Role', 'role_user_table', 'user_id', 'role_id');

其中,那个中间连接表,可以被一个pivot (轴承) 方法获取,如

$user = App\User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
} // 每一个实例都有一个pivot 方法指向相应连接表的记录

中间表也可以有自己的model,but it need inherit “Pivot” instead of “Model”

使用时也要using model rather than the table

return $this->belongsToMany('App\User')->using('App\UserRole');
A to B to C

case: C table has a b_id column, B tabel has a a-id column, then we can use hasManyThrough to get C from A

class A extends Model {
  public function C() {
  	return $this->hasManyThrough('App\C', 'App\B');
    // App\C is the final data we want
    // App\B is the intermedia table we use
  }
}
Polymorphic 1 to Many

Case: comments table can be used to both posts and videos

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string
class Comment extends Model
{
  	// Get all of the owning commentable models.
    public function commentable()
    {
        return $this->morphTo();
    }
}
<------------------------------------>
class Post extends Model
{
  	// Get all of the post's comments.
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}
<------------------------------------>
class Video extends Model
{
  	// Get all of the video's comments.
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

$post = App\Post::find(1);

foreach ($post->comments as $comment) {...}
Polymorphic many to many

case: posts and videos can have many tags, we use the “taggables” table conect them

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string
class Post extends Model
{
  	// Get all of the tags for the post.
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}
Eager Load

在有很多很多联系的时候,我们可以用eager load 来快速获取相关的信息

如下例子,Card 对应很多 Note, 每一个Note 对应一个 Author

$card = APP\Card->find(1);

//我们可以通过如下方式取得 $card 的对应的 users
return $card->notes[0]->user;

//也可以使用eager load 取得同样的效果
return $card->load('notes.user');

//Eager Load, join 两张表获取所有的信息
$card->with('note')->get();

8. Eloquent Create Records

在上面例子里,我们可能需要创建新的note,这时我们需要在note model 里面允许编辑,如下

class Note extends Model
{
    protected $fillable = ['body'];

    public function card(){
        return $this->belongsTo(Card::class);
    }
}

$fillable 使得我们有权限编辑 ’body‘ 字段

这样我们在tinker 里可以使用$card->notes()->create(['body'=>'yet another note about this card']); 来为card添加note,也就可以使用在页面逻辑中

9. Test Data

laravel 允许我们通过factories 工具构建大量测试数据

  • 在生成model 后,可以在database->factories->ModelFactory.php 中构建想要的数据

  • 之后可以在make seed 或者tanker 中生成数据,例如在tinker中使用:

    factory('App\Task', 20)->make() 就可以生成20条tasks

    factory('App\Task', 20)->create() 就可以生成20条tasks 并存入数据库

Route

这里需要总结一下Route了,总结一下一个请求的过程中数据的走向

  1. 这样的一个地址,http://learnlaravel:1234/cards/13 laravel 会自动寻找 Card Model

    如果有, 那么

    • Router: Route::get('cards/{card}', 'CardsController@show');
    • Controller: public function store(Request $request, Card $card){..}
  2. 在上面这个过程里,浏览器发起请求,{card} 被赋值为 13;

  3. controller 去Card Model 里面找 数据库cards 表里,id 为1的那个数据 ,提取出来赋给 $card

  4. controller 拿到 $card 后,想返回到一个view 里:

     return view('card.whatever', compact('card'));
    

    $card 就被传递给 whatever 这个view里面了。

    这个时候,在这个view 里面,我们就可以直接使用

    {{$card->XXX}} 去调用数据了

Form

  1. 在view 里添加 form 表单

    show.blade.php:

    <form method="POST" action="/cards/{{ $cardId->id }}/notes">
      <div class="form-group">
        <textarea name="body" class="form-control"></textarea>
        {{--for CSRF Protection--}}
        <input type="hidden" name="_token" value="{{ csrf_token() }}">
      </div>
      <div class="form-group">
        <button type="submit" class="btn btn-danger" >Add Note</button>
      </div>
    </form>
    

    这样的例子会post 一个请求到 localhost/cards/2/notes

    post 的内容是一个JSON: {body: textarea content, _token: ……}

  2. 所以需要一个对应的路由去接管 “ localhost/cards/2/notes ” 这个地址

    Route::post('cards/{cardId}/notes', 'NotesController@store');
    

    因为是post请求,所以前缀要有post,对应到一个新的controller 下面的store 方法

  3. (如果是新建controller,用 $php artisan make:controller NotesController )

    新建store方法:

    use App\Card;
    use App\Note;
    ...
      public function store(Request $request, Card $cardId){
    
      //another way is " return /Request::all(); "
      //return $request->all();
    
      //Approach 1:
      //$note = new Note;
      //$note->body = $request->body;
      //$cardId -> notes() -> save($note);
    
      //Approach 2:
      //$cardId -> notes() -> save(
      //    new Note(['body' => $request->body])
      //);
    
      //Approach 3:
      //$note = new Note(['body' => $request->body]);
    
      //Approach 4:
      //$cardId -> notes() -> create({
      //  'body' => $request->body;
      //})
    
      //Approach 5:
      $cardId -> notes() -> create($request->all());
      return back();
    
      //or return redirect()->to('url');
    }
    
    • 有多种方法可以取到request内容,或者直接添加到card 中

    • 需要 $cardId 信息需要先添加 use App\Card

    • 最后一种方法需要注意,一般的php 不能这样操作,因为会加入其他信息,如_token。

      但是laravel 允许这样做因为我们在之前限定了 $fillable = ['body'] 所以只有body会被提交

Validate the Form

  1. 在获得用户id 的时候,建议不要直接在Html 里写 <input type="hidden" name="user_id" value="xxx"> , 因为用户可以在提交表单的时候通过修改html 表单去伪装登陆。

    这种情况下,我们可以在保存note 的方法里写入user 信息,如下

    NotesController:

    public function store(Request $request, Card $cardId){
      $note = new Note($request->all());
      $note->user_id = Auth::user_id;
      $cardId -> notes() -> save($note);
      return back();
    }
    

    这样用户就无法自己修改登录用户信息了

  2. 在store note 的时候,有时需要保证一些规则,如某些值不为空等等

    可以查看validate 文档,有很多功能,这里简单的写个例子

    NotesController -> store

    $this->validate($request, [
      'body' => 'required|min:10'     // check "Laravel Available Validation Rules"
    ]);
    

    这样body 输入的内容不合法的时候不会保存,会返回原来页面,可以通过全局变量 $error 来查看错误信息

    也可以在Html 上显示错误信息,如下例

    @if(count($errors))
      <div class="alert alert-warning" role="alert">
        @foreach($errors->all() as $error)
        <p>{{ $error }}</p>
        @endforeach
      </div>
    @endif
    

Core Concept

IoC (Inversion of Control)

用来降低代码间的耦合度

理解

  • 类A 要依赖BCDEF,所以一般来说,需要在实例化 A 的时候同时实例化BCDEF

  • 但是控制反转究竟反转了什么呢?依赖对象的获得被反转了

  • In step1,A 获取了 BCDEF,A 可以随意创建或者使用BCDEF,主动权在A,但是高度耦合

  • 但是通过控制反转,我们通过第三方,在外部实例化好BCDEF,注入到A 的五个私有对象中,降低耦合

  • 此时A 不再有主动权(因为它不控制BCDEF),控制权在第三方手里,他在A 需要的时候实例化一个B 实例给A,所以称作控制反转

超人的例子

  • 很累的超人

    class Superman
    {
        protected $power;
    
        public function __construct()
        {
            $this->power = array(
              	new Fight(9, 100),
                new Force(45),
                new Shot(99, 50, 2)
            );
        }
    }
    

    一般的超人,很厉害但是很累,一旦修改了power 的参数,例如现在fight(speed, holdtime) 变成了 flight(strength, speed, holdtime),这样我们不仅要修改flight 类,同时要修改超人类

  • 用个工厂模式吧

    class SuperModuleFactory
    {
        public function makeModule($moduleName, $options)
        {
            switch ($moduleName) {
                case 'Fight':     return new Fight($options[0], $options[1]);
                case 'Force':     return new Force($options[0]);
                case 'Shot':     return new Shot($options[0], $options[1], $options[2]);
            }
        }
    }
    class Superman
    {
        protected $power;
    
        public function __construct()
        {
            // 初始化工厂
            $factory = new SuperModuleFactory;
    
            // 通过工厂提供的方法制造需要的模块
            $this->power = array(
                $factory->makeModule('Force', [45]),
                $factory->makeModule('Shot', [99, 50, 2])
            );
        }
    }
    

    好像也没有太大区别,只是不用写很多new 关键字,其实再换一种写法就清楚了

  • 新写法 你会懂

    class Superman
    {
        protected $power;
    
        public function __construct(array $modules)
        {
            // 初始化工厂
            $factory = new SuperModuleFactory;
    
            // 通过工厂提供的方法制造需要的模块
            foreach ($modules as $moduleName => $moduleOptions) {
                $this->power[] = $factory->makeModule($moduleName, $moduleOptions);
            }
        }
    }
    
    // 创建超人
    $superman = new Superman([
        'Fight' => [9, 100], 
        'Shot' => [99, 50, 2]
        ]);
    

    这样就好很多了。我们不用改写超人类了,只需要在建造超人的时候选择power 就好,很简单

Dependcy Injection

控制反转是一种思想,而依赖注入是实现它的一种途径。

Laravel 通过接口实现依赖注入

  • 超能力接口

    interface SuperModuleInterface
    {
        /**
         * 超能力激活方法
         *
         * 任何一个超能力都得有该方法,并拥有一个参数
         *@param array $target 针对目标,可以是一个或多个,自己或他人
         */
        public function activate(array $target);
    }
    
    class XPower implements SuperModuleInterface
    {
        public function activate(array $target){...}
    }
    
    class UltraBomb implements SuperModuleInterface
    {
        public function activate(array $target){...}
    }
    

    所有的超能力都必须走这个借口,这样保证了超能力的格式一致性,不管你是什么超能力,都可以被超人接受。

  • 超人对接接口

    class Superman
    {
        protected $module;
    
        public function __construct(SuperModuleInterface $module)
        {
            $this->module = $module
        }
    }
    
    // 超能力模组
    $superModule = new XPower;
    
    // 初始化一个超人,并注入一个超能力模组依赖
    $superMan = new Superman($superModule);
    // or
    $superManNo2 = new Superman(UltraBomb $ultraBomb)
    

    现在超人只接受一个SuperModuleInterface 对象作为参数,其他类型的参数都会报错。

    至此依赖注入结束,我们从超人类自己手动定义依赖,到自己选择想要的依赖进行注入,彻底解耦。

IoC Container

上述的生产方法还是需要 实例化 一个超能力对象,再注入到SuperMan 里,手动行还是比较多,不够自动。

是时候来一个更高级的工厂模式 - IoC Container (laravel 中叫做 service container)

// 一个最简陋的IoC 模型
class Container {
  protected $binds;			// array,绑定的类
  protected $instances;		// array,类的实例化集合
  
  /**
  	*	向 Container 添加/绑定一种对象的生产方式
  	*
    *	@abstract 	一个字符串(或接口), 当你需要 make 这个类的对象的时候, 传入这个字符串(或者接口)
    *				这样make 就知道制造什么样的对象了
    *	@concrete	一般为一个 Closure 或者 一个单例对象, 用于说明制造这个对象的方式
  **/
  public function bind ($abstract, $concrete) {
    if ($concrete instanceof Closure) {		// 闭包,即检查 $concrete 的值是不是一个匿名函数
      $this->binds[$abstract] = $concrete;	// 若是闭包,添加到binds 集合中
    } else {
      $this->instances[$abstract] = $concrete;	// 若是实例,说明是实例对象,添加到实例集合中
    }
  }
  
    /**
  	*	通过上述的绑定,生产相应的对象
  	*
    *	@abstract 	与上述相同,字符串或接口表示这个类的名称
    *	@parameters	array 在生产这个类时需要的参数,默认为空
  **/
  public function make ($abstract, $parameters = []) {
    if (isset($this->instances[$abstract])) {
      return $this->instances[$abstract];
    }
    array_unshift($parameters, $this);
    return call_user_func_array($this->binds[$abstract], $parameters)
  }
}

上述就是一个最简单的容器,有bind 和make 方法,使用如下

$container = new Container;		// 创建一个上面的container

// 绑定 superman,bind 后面接一个闭包,返回 SuperMan 加 make
$container->bind('superman', function ($container, $moduleName)) {
  return new SuperMan($container->make($modoleName));
}

// 绑定技能,bind 后面接实例,往superman 里添加能力
$container->bind('xpower', function ($container) {
  return new XPower;
});
$container->bind('ultrabomb', function($container) {
    return new UltraBomb;
});

// ******************  华丽丽的分割线  **********************
// 开始启动生产
$superman_1 = $container->make('superman', 'xpower');
$superman_2 = $container->make('superman', 'ultrabomb');
$superman_3 = $container->make('superman', 'xpower');
// ...随意添加

这个时候Superman 和 技能彻底解耦,没有相互依赖的关系,只需要通过注册、绑定的方法向容器里添加一段可以被执行的回调,作为生产一个类实例的脚本,就可以通过make 方法去实时调用。

至此,Laravel 核心IoC Container 基本建立,其他的功能模块,如Route, Eloquent ORM, Request and Response 等都是与核心无关的模块,这些类从注册到实例化,都是容器负责。

Service Provider

Laravel 的核心是上面说的IoC Container,但是它封装了注册的过程,用一个简单的 ServiceProvider 去完成绑定注册的过程。

每一个Module 都有一个对应的XXXServiceProvider 继承ServiceProvider,用于注册和绑定。 ServiceProvider 包括连个部分,Register 注册Boot 初始化

  • Register 负责向容器注册脚本
  • Boot 会在所有Module 注册之后调用,所以在这里可以调用其他的Module

有了相应的XXX ServiceProvider 之后,我们在 Config\app 中的 Provider 对象里面注册每一个module 的 ServiceProvider, 如 AuthServiceProvider, RouteServiceProvider 甚至 BentoboxServiceProvider 等,真正把service 注册到container中。

Others

Authentication

一般在项目最初的时候就应该进行,不建议在项目一半的时候进行

  1. $php artisan make:auth
  2. config database, and migrate by default
Session
  • session flash: 只在访问特定url时出现一次,刷新就没有了,可用于用户刚注册完成的界面

    Route::get('begin', function(){
        Session::flash('status', 'hello');
        return redirect('/home');
    });
    // 在对应的home view 里面,应该包含如下:
    @if(Session::has('status'))
    	<h3>{{ Session::get('status') }}</h3>  
    @endif
    

MiddleWare

middleware 可以用来控制访问的一个中间连接件

举例来说,用户登录可以看到自己的信息,但是没有登录的用户,如果访问了查看信息的url,需要一层中间件来把他隔离出来。Laravel 自带的Auth 模块很好的利用middleware 解决了这个问题

app -> http -> middleware -> authenticate.php:

class Authenticate
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->guest()) {
            if ($request->ajax() || $request->wantsJson()) {
                return response('Unauthorized.', 401);
            }

            return redirect()->guest('login');
        }

        return $next($request);
    }
}

解析:

1. 如果用户发起一个请求,判断是不是`guest()` 
2. 如果是,判断是不是要请求信息或数据
 - 如果是,返回401 Unauthorized
 - 如果不是,返回到login 页面要求登录
3. 如果不是,说明是已经登录的用户,使用$next 进行下一步操作

如何使用?

就上面例子来说,我们可以在 app -> http -> Kernel.php 里面看到如下

 protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
   		...
    ];

上面的Authenticate 中间件被赋了一个名字 ‘auth’

  • 方法一:

    这样,可以在router.php 里写如下:

    Route::get('dashboard', 'HomeController@index') -> middleware('auth');

    这样在访问dashboard 的时候,会先访问middleware auth,检查是不是登录,再进行下一步

  • 方法二 (推荐):

       这样,可以在需要的controller里,如HomeController.php 开头写下:
    
                      class HomeController extends Controller
                      {
                          /**
    * Create a new controller instance.
    *
    * @return void
    */
    
    public function __construct()
    {
        $this->middleware('auth');
    }
    

    … }

    
    这样HomeController 下面所有的方法在被调用时都会先走中间件auth
    
    当然还可以用类似 `$this->middleware('auth',['except' => ['index', 'xxx']])` 来排除某些方法
    
    或者用 `$this->middleware('auth', ['only'=>['index', 'xxx'])` 来指定使用某些方法
    
    
自定义一个middleware
  1. $php artisan make:middleware MustBeAdmin

  2. app -> http -> middleware -> mustbeadmin.php

    public function handle($request, Closure $next)
    {
      $user = $request -> user();
    
      if($user && $user->name == 'Ethan'){
        return $next($request);
      }
    
      abort(404, "no way");
    }
    

1. app -> http -> kernel.php

 ```php
 protected $routeMiddleware = [
   ...
   'admin' => \App\Http\Middleware\MustBeAdmin::class,
   }
  1. certain controller:

    public function __construct()
    {
      $this->middleware('admin');
    }
    

放在global HTTP middleware 会自动套用在所有route 上

依赖注入 (Dependency Injection)

当使用的类拥有非常多的依赖类时,调用(new) 这个类就会需要把它依赖的所有类new 一遍,如下

class UserRegis() {
  protected $mailer
  public function __construct(Mailer $mailer){
    $this->mailer = $mailer;
  }
}

//in other places
$userregis = new UserRegis(new Mailer(new Foo(new Bar)))

这样在依赖很多的时候就很复杂

所有Laravel 利用依赖注入的方式,简化了这个流程,在app service provider 里面绑定依赖即可,如

App::bind('UserRegis', function(){
  return new UserRegis(new Mailer(new Foo(new Bar)))
})
  
//in other place
$userregis = app('UserRegis')
//or
public function test(UserRegis $userregis){
 $userregis.... 
}

bind 表示所有实例各自独立,也可以用singleton方法,表示所有new的结果都是同一个实例

其实,如果都是实体类,可以省略绑定的步骤,直接public function test(UserRegis $userregis){} 也能找到这个类,但是如果使用的是抽象类或者接口,那么就一定需要去绑定一次。

Interface

接口(interface) 就像是RPG游戏中的装备系统,比如说“防具”,“武器”,“魔法”,就是所谓的接口,你可以装备 各种对应的道具,比如“橡木盾”,“屠龙刀”,“亡灵法杖”,这些就是实现(implementation)。接口的作用就是解耦,简单理解就是“装备”,实现就是具体装备的实体类,只要是按照接口换装,英雄不会出现无法使用武器的情况。

Laravel 里,首先要定义一个Interface,里面有需要实现的方法,如

namespace App\Api;

interface DiorApiInterface {
    public function test();
    public function isMember($openId, $mobile);
    public function getMemberProfile($memberId);
    public function processGiftReservation($params);
    public function getActiveRedemptionGiftList($giftId);
}

然后要有至少一个实现类去实现这个接口,接口里的每一个类都需要实现,如

class Case1 implements DiorApiInterface{
    public function test(){
        return "this is DiorApi Class";
    }
  ......
}

再然后,要在app service provider 里面绑定接口

    public function boot() {
        $this->app->bind('App\Api\DiorApiInterface', function(){
            if(Env::isDebug()){
                return new Case1();
            }
            return new Case2();
        });
    }

自动化工具 Gulp in Laravel

不能相信 laravel 居然连gulp 也集成进来了 ORZ,而且主要功能封装的很简单

  1. 安装

    使用 npm install 去安装 package.json 里依赖的包 ORZORZORZ

  2. 在gulpfile.js 里,设置需要的自动化内容,如

    var elixir = require('laravel-elixir');
    
    elixir(function(mix) {
        mix.sass('app.scss');
    });
    

    exlixir 是 laravel 集成的一个工具,简化了自动化操作

    上述自动化设置了将 resources -> asset -> app.scss 自动编译的功能

    只需要在laravel 项目文件夹路径内,使用 “gulp” 命令,就会自动执行

    • 使用 gulp —production 命令,还会自动加上压缩功能
    • 使用 gulp watch 命令,就会自动watch… 不能更方便了…
  3. 还集成了很多其它功能,参见 https://laravel.com/docs/5.2/elixir

artisan 工具

Artisan 是 Laravel 内建的命令行工具,它提供了一些有用的命令协助您开发

在laravel 目录下使用 php artisan list 查看命令列表

可以创建server,

创建controller php artisan make:controller

可以用 php artisan help make:controller 来查看命令的使用方法

  • Maintain mode

    $php artisan down 会使网站进入维护界面,期间可以修改数据库等操作

    $php artisan up 网站上线

Helper 自定义工具集

有时候需要很多自定义的工具函数需要随时调用,laravel 提供了一个方法可以全局调用这写方法

  1. 在APP 下新建一个PHP文档,如helper.php,并写入自己的工具函数

  2. 在composer.json 里, 找到autoload,写入:

    "autoload": {
      ...,
      "files":[
        "app/helper.php"
      ]
    },
    
  3. $composer dump-autoload

完成

Static HTML Cache

将静态页面定期每小时生成缓存,利用 .htaccess 重定向功能使静态页面的访问直接走缓存,加快存取速度。

How To

  1. 配置middleware,定期生成缓存

    • php artisan make:middleware CacheStaticHtmlc 新建middleware

    • 查找缓存路径,如果没有找到(该时段第一次访问),则删除所有已过期缓存,新建HTML并缓存

      public function handle(Request $request, Closure $next) {
        if ($request->method() == 'GET') {
          $response = $next($request);
          $day = str_pad(date('d'), 2, '0', STR_PAD_LEFT);
          $hour = str_pad(date('H'), 2, '0', STR_PAD_LEFT);
          $url = preg_replace('/https?:\/\//', '', $request->url());
          $cacheDirectory = public_path() . '/cache/html/';
          $directory = $cacheDirectory . $day . '/' . $hour . '/' . $url;
      
          //if no such file, delete all expired cache, then create a new cache
          if (!File::isDirectory($directory)) {
            if (File::isDirectory($cacheDirectory)) File::deleteDirectory($cacheDirectory);
            File::makeDirectory($directory, 0777, true);
            File::put($directory . '/index.html', $response->getContent());
          } 
        }
        return $next($request);
      }
      
    • 在kernel.php 中注册middleware

      'cacheStatic' => [
        CacheStaticHtml::class,
      ],
      
  2. 配置 .htaccess 文件

    如果改地址有缓存文件存在,则不经过路由,直接读取缓存HTML

    RewriteEngine On
    
    # Rewrite to html cache if it exists and the request is off a static page
    # (no url query params, no session cookies and only get requests)
    # A new cache will be created every hour
    RewriteCond %{REQUEST_METHOD} GET
    RewriteCond %{DOCUMENT_ROOT}/cache/html/%{TIME_DAY}/%{TIME_HOUR}/%{HTTP_HOST}/$1/index.html -f
    RewriteRule ^(.*)$ cache/html/%{TIME_DAY}/%{TIME_HOUR}/%{HTTP_HOST}/$1/index.html [L]
    
  3. 将需要缓存的路由放在该middleware下

    Route::group(['middleware' => ['cacheStatic']], function () {
        // 专属资讯
        Route::get('/mc/home', 'DiorMemberHomeController@index');
        // 专属资讯 -> 专柜活动
        Route::get('/mc/activity', 'DiorMemberHomeController@activity');
        // 专属资讯 -> 会员优享活动
        Route::get('/mc/advantage', 'DiorMemberHomeController@advantage');
        // 专属资讯 -> 当季热品
        Route::get('/mc/products', 'DiorMemberHomeController@products');
        // 专属资讯 -> 当季热品 -> 热品详情
        Route::get('/mc/products/{id}', 'DiorMemberHomeController@productsDetail');
    });
    

Notice

  • Nginx 服务器可以使用 该网址 自动生成htaccess

  • 在更新静态页面时,记得及时删除cache 文件夹重新生成新缓存。